﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;
using System.Text;
using vJine.Core;
using vJine.Core.Base;
using vJine.Core.IO.Json;
using vJine.Core.IoC;

namespace vJine.Core.Web {
    public class HttpClient<Tparam> : HttpClient {
        public string Parse(Tparam arg, Dictionary<string, string> headers = null, Action<string> callback = null) {
            return this.handle(headers, callback);
        }
    }

    public class HttpClient<Tparam, Tresult> : HttpClient {
        public Tresult Parse(Tparam arg, Dictionary<string, string> headers = null, Action<Tresult> callback = null) {
            return JsonHelper.Parse<Tresult>(this.handle());
        }
    }

    public class HttpClient {
        #region base_config
        private string _host;
        public string Host {
            get {
                return this._host;
            }
            set {
                if(value != this._host) {
                    this._host = value;
                }
            }
        }

        public int _port;
        public int Port {
            get {
                return this._port;
            }
            set {
                if(value != this._port) {
                    this._port = value;
                }
            }
        }

        private string _url;
        public string Url {
            get {
                return this._url;
            }
            set {
                if(value != this._url) {
                    this._url = value;
                }
            }
        }

        private string _uri;
        public string uri {
            get {
                return this._uri;
            }
            protected set {
                if(value != this._uri) {
                    this._uri = value;
                }
            }
        }

        int _read_timeout;
        public int read_timeout {
            get {
                return this._read_timeout;
            }
            set {
                if(value != this._read_timeout) {
                    this._read_timeout = value;
                }
            }
        }

        int _write_timeout;
        public int write_timeout {
            get {
                return this._write_timeout;
            }
            set {
                if(value != this._write_timeout) {
                    this._write_timeout = value;
                }
            }
        }
        #endregion base_config

        public HttpClient()
            : this("127.0.0.1", 80, "/") {
        }
        
        public HttpClient(
            string host, int port, string url, int read_timeout = 10, int write_timeout = 10) {

            this.Host = host; this.Port = port; this.Url = url;
            this.read_timeout = read_timeout;
            this.write_timeout = write_timeout;
        }

        public static void handle(string host, int port, string url) {
            HttpClient client = new HttpClient(host, port, url);
            client.set_headers("Accept-Language", "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3");
            client.handle();
        }

        /*
         * 支持chunk下载
         * open_stream,close_stream,keep-alive
         * 自定义User-Agent,自定义Header
         * 
         * Header<编码，解码>
         * *************************
         * 集成HttpBase, 合并解析头部分
         * *************************
         * 200, 301, 302, 401, 404, 500未响应
         * *************************
         * 返回的文件类型及文件大小，Html, Json, Xml, Raw
         * *************************
         * 1）API请求
         * 2）HTML解析
         * 3）全站扫描
         * *************************
         * PHP-FCGI
         * **************************
         * 参数值ToString及Parse（简单型及类类型：Json或自定义)
         * FROM数组：P,Q,C,F(文件数组，可字符，可Stream), 如果以[]结尾则添加为数组
         * **************************
         * 协议转换：支持Update
         * **************************
         * 多家API支持
         * 生成客户端调用代码（CS、BS）
         */

        byte[] BUFFER_WR = new byte[8192];

        Stream bufferHeader = new MemoryStream();
        Stream bufferContent = new MemoryStream();
        
        static string User_Agent = "vJine.Core/" + vJine.Core.Utility.getVersion<HttpClient>();
        public string handle(Dictionary<string, string> responseHeaders = null, Action<string> callback = null) {
            if(responseHeaders == null) {
                responseHeaders = new Dictionary<string, string>();
            } else {
                responseHeaders.Clear();
            }

            int data_length_write = 0, data_length_read = 0; //全部长度
            int content_length_write = 0, content_length_read = 0; //数据包长

            lock(this) {
                this.open_stream();

                bool IsGet = !(this.FILE.Count > 0 || this.PARAM.Count > 0);

                if(this.Url.StartsWith("/")) {
                    this.Url = this.Url.Substring(1);
                }
                this.uri = 
                    "http://" + this.Host + ":" + this.Port + "/" + this.Url +
                    this.get_key_value_string(this.QUERY, "=", "&");

                this.set_headers("Host", this.Host);
                this.set_headers("Accept-Encoding", "gzip, deflate");
                this.set_headers("User-Agent", HttpClient.User_Agent, false);
                this.set_headers("Cookie", this.get_key_value_string(this.COOKIE, "=", ";")); //TODO:回调方法，当写缓存Cookie时调用
                this.set_headers("Connection", "Keep-Alive");

                Action<bool> write_headers = (bool flush) => {
                    data_length_write += this.write("{0} {1} HTTP/1.1\r\n", IsGet ? "GET": "POST", this.uri); //TODO:回调方法，当写Uri是调用
                    data_length_write += this.write(this.get_key_value_string(this.HEADER, ": ", "\r\n", true));
                    data_length_write += this.write("\r\n");
                    if(flush) {
                        this.flush();
                    }
                };

                if(IsGet) {//GET
                    write_headers(true);
                } else if(this.FILE.Count == 0) {//POST：无文件
                    this.set_headers("Content-Type", "application/x-www-form-urlencoded");

                    string post_data = this.get_key_value_string(this.PARAM, "=", "&");
                    byte[] post_content = Encoding.ASCII.GetBytes(post_data);
                    this.set_headers("Content-Length", post_content.Length.ToString());

                    write_headers(false);

                    this.write(post_content);
                    this.flush();

                } else { //POST：包含文件
                    /* 以下均计入Content-Length
                    * 0)起始--boundary，1)每个Header有换行，2)Content开始前有换行，3)Content结束有换行，4)结束boundary--，5)换行
                     * 6）P,F为空也可POST,POST为Json对象
                    */
                    string boundary = "---------------------------136972610514073";
                    string beginBoundary = "--" + boundary + "\r\n";
                    byte[] endBoundary = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");

                    this.set_headers("Content-Type", "multipart/form-data; boundary=" + boundary);

                    //计算长度：POST DATA
                    string post_data =
                        this.get_key_value_string(this.PARAM, "\r\n", "\r\n", true, beginBoundary + "Content-Disposition: form-data; name={0}");
                    byte[] POST_DATA = Encoding.UTF8.GetBytes(post_data);
                    content_length_write = POST_DATA.Length;

                    //计算长度：POST FILE
                    List<byte[]> file_headers = new List<byte[]>();
                    List<FileInfo> FI = this.get_files(Encoding.UTF8, file_headers, ref content_length_write);
                    content_length_write += endBoundary.Length;

                    //设置长度
                    this.set_headers("Content-Length", content_length_write.ToString());

                    {
                        //上传：头
                        write_headers(false);
                        //上传：POST DATA
                        this.write(POST_DATA);
                        //上传：文件

                        for(int i = 0, len = FI.Count; i < len; i++) {
                            byte[] HEADER = file_headers[i];
                            this.write(HEADER); //写：文件头
                            FileStream fileStream =
                                FI[i].Open(FileMode.Open, FileAccess.Read, FileShare.Read);
                            this.write(fileStream, this.BUFFER_WR, (int)FI[i].Length);
                            fileStream.Close();
                            this.write("\r\n"); //写：文件结束符
                        }
                        //上传：结束标记
                        this.write(endBoundary);
                        this.flush();
                    }
                }

                //HTTP/1.1 200 OK
                string status = this.get_headers(responseHeaders, true);
                if(string.IsNullOrEmpty(status)) {
                    throw new CoreException("状态行为空:[{0}]", status);
                } else if(status.IndexOf(" 200") > 0) {
                    //返回成功，继续
                } else {
                    throw new CoreException("未受支持的状态:[{0}]", status);
                }

               /*
                * Content-Length: 1234
                * Transfer-Encoding: chuncked
                * Content-Encoding: gzip
                * Content-Type: texm/html;charset=gb2312
                */
                content_length_read =
                    this.get_value<int>(responseHeaders, "content-length", -1);
                bool isChunked =
                    this.get_value<string>(responseHeaders, "transfer-encoding", "").ToLower() == "chunked";

                this.bufferContent.Position = 0;
                this.bufferContent.SetLength(0);

                string dummy_line = string.Empty;

                if(content_length_read > 0) {
                    this.read(this.bufferContent, this.BUFFER_WR, content_length_read);
                } else if(isChunked) {
                    do {
                        string L = this.read();
                        data_length_read += L.Length + 2;
                        if(L == "0") {
                            break;
                        }

                        int len_block = Convert.ToInt32(L, 16);
                        this.read(this.bufferContent, this.BUFFER_WR, len_block);
                        content_length_read += len_block;
                        data_length_read += len_block;
                        //每个Block后有一个空行
                        dummy_line = this.read();
                        data_length_read += 2;
                        if(!string.IsNullOrEmpty(dummy_line)) {
                            throw new CoreException("CHUNK 格式错误");
                        }
                    } while(true);
                    //结尾有一个空行
                    dummy_line = this.read();
                    data_length_read += 2;
                    if(!string.IsNullOrEmpty(dummy_line)) {
                        throw new CoreException("CHUNK 格式错误");
                    }
                } else {
                    content_length_read = this.read(this.bufferContent, this.BUFFER_WR);
                    data_length_read += content_length_read;
                }

                string content_encoding = this.get_value<string>(responseHeaders, "Content-Encoding", "");
                bool isGzipped = content_encoding.IndexOf("gzip") >= 0;
                bool isDelflated = content_encoding.IndexOf("delflate") >= 0;
                Encoding contentEncoding =
                    Encoding.GetEncoding(this.get_value<string>(responseHeaders, "content-type", "charset", "GB2312"));

                string result =
                    this.get_string(this.bufferContent, contentEncoding, (isGzipped || isDelflated) ? isGzipped : (bool?)null);

                this.nextAlive = 
                    this.get_value<string>(responseHeaders, "connection", "close").ToLower() == "keep-alive";
                this.close_stream(this.nextAlive);

                if(callback != null) {
                    callback(result);
                }
                return result;
            }
        }

        TcpClient myClient = null;
        NetworkStream net_stream = null;
        bool nextAlive = false;
        void open_stream() {
            Exec createStream = () => {
                this.myClient = new TcpClient();
                this.myClient.Connect(this.Host, this.Port);
                this.net_stream = this.myClient.GetStream();
            };

            if(this.net_stream == null) {
                createStream();
            } else {
                try {
                    this.net_stream.Write(this.BUFFER_WR, 0, 0);
                } catch {
                    this.close_stream();
                    createStream();
                }
            }
        }

        void close_stream(bool keep_connect = false) {
            if(keep_connect) {
                return;
            }
            if(this.net_stream != null) {
                try {
                    this.net_stream.Flush();
                    this.net_stream.Close();
                    this.net_stream = null;
                } catch {
                }
            }
            if(this.myClient != null) {
                try {
                    this.myClient.Close();
                    this.myClient = null;
                } catch {
                }
            }
            
        }

        Encoding headerEncoding = Encoding.ASCII;
        int write(string content, params object[] Args) {
            content = string.Format(content, Args);
            int len_bytes = this.headerEncoding.GetBytes(content, 0, content.Length, this.BUFFER_WR, 0);
            return this.write(this.BUFFER_WR, len_bytes);
        }

        int write(byte[] write_buffer, int length = 0, int offset = 0) {
            offset = offset <= 0 ? 0 : offset;
            length = length <= 0 ? write_buffer.Length - offset : length;
            
            this.net_stream.Write(write_buffer, offset, length);

            return length;
        }

        void write(Stream stream_from, byte[] buffer, int length) {
            vJine.Core.Utility.trans(stream_from, this.net_stream, buffer, length);
        }

        byte[][] NL = new byte[][] { HttpBase.ASCII.GetBytes("\r\n") };
        string read() {
            return Encoding.ASCII.GetString(Utility.Read(this.net_stream, 1024, NL)).TrimEnd('\r', '\n');
        }

        int read(byte[] read_buffer, int length = 0, int offset = 0) {
            offset = offset <= 0 ? 0 : offset;
            length = length <= 0 ? read_buffer.Length - offset : length;

            int length_read = 0, read_this, read_max = 0;
            do {
                read_this = this.net_stream.Read(read_buffer, offset, read_max);
                offset += read_this;
                length_read += read_this;
            } while(length_read < length);

            return length_read;
        }

        int read(Stream stream_to, byte[] buffer, int length = 0) {
            return vJine.Core.Utility.trans(this.net_stream, stream_to, buffer, length);
        }

        int trans(byte[] buffer, Stream stream_to, int offset = 0, int length = 0) {
            offset = offset <= 0 ? 0 : offset;
            length = length <= 0 ? buffer.Length - offset : length;

            stream_to.Write(buffer, offset, length);
            return length;
        }

        string get_string(Stream contentStream, Encoding encoding, bool? decoding = null) {
            if(contentStream.Length == 0) {
                return null;
            }
            contentStream.Position = 0;

            byte[] buffer = null;
            if(decoding == null) {
                buffer = new byte[contentStream.Length];
                contentStream.Read(buffer, 0, (int)contentStream.Length);
            } else {
                buffer = this.deCompress(decoding.Value);
            }
            
            return encoding.GetString(buffer);
        }

        MemoryStream bufferDeCompress = new MemoryStream();
        byte[] deCompress(bool isGzip = true) {
            this.bufferContent.Position = 0;
            this.bufferDeCompress.Position = 0;
            Stream deComressStream = null;
            if(isGzip) {
                deComressStream = new GZipStream(this.bufferContent, CompressionMode.Decompress);
            } else {
                deComressStream = new DeflateStream(this.bufferContent, CompressionMode.Decompress);
            }

            vJine.Core.Utility.trans(deComressStream, this.bufferDeCompress, this.BUFFER_WR);

            this.bufferDeCompress.Position = 0;
            return this.bufferDeCompress.ToArray();
        }

        void flush() {
            if(this.net_stream == null) {
                return;
            }

            this.net_stream.Flush();
        }

        void set_key_value(Dictionary<string, string> kvContainer, string name, string value, bool overwrite = true) {
            if(kvContainer.ContainsKey(name)) {
                if(overwrite) {
                    kvContainer[name] = value;
                }
            } else {
                kvContainer.Add(name, value);
            }
        }

        Dictionary<string, string> HEADER = new Dictionary<string, string>();
        public void set_headers(string name, string value, bool overwrite = true) {
            this.set_key_value(this.HEADER, name, value, overwrite);
        }

        Dictionary<string, string> COOKIE = new Dictionary<string, string>();
        public void set_cookie(string name, string value, bool overwrite = true) {
            this.set_key_value(this.COOKIE, name, value, overwrite);
        }

        Dictionary<string, string> QUERY = new Dictionary<string, string>();
        public void set_query(string name, string value, bool overwrite = true) {
            this.set_key_value(this.QUERY, name, value, overwrite);
        }

        Dictionary<string, string> PARAM = new Dictionary<string, string>();
        public void set_param(string name, string value, bool overwrite = false) {
            this.set_key_value(this.PARAM, name, value, overwrite);
        }

        Dictionary<string, string> FILE = new Dictionary<string, string>();
        public void set_file(string name, string value, bool overwrite = false) {
            this.set_key_value(this.FILE, name, value, overwrite);
        }

        T get_value<T>(Dictionary<string, string> kvContainer, string name, T value) {
            name = name.ToLower();
            if(kvContainer.ContainsKey(name)) {
                return Class.Parse<T>(kvContainer[name]);
            } else {
                return value;
            }
        }

        T get_value<T>(Dictionary<string, string> kvContainer, string name, string sub_key, T value) {
            name = name.ToLower();

            if(kvContainer.ContainsKey(name)) {
                string tempValue = kvContainer[name];
                if(string.IsNullOrEmpty(tempValue)) {
                    return value;
                } else if(tempValue.IndexOf(sub_key) < 0) {
                    return value;
                } else {
                    int subIndex =
                        tempValue.IndexOf(sub_key) + sub_key.Length;
                    int subLast = tempValue.IndexOf(';', subIndex);
                    if(subLast < 0) {
                        tempValue = tempValue.Substring(subIndex).Trim().TrimStart('=');
                    } else {
                        tempValue = tempValue.Substring(subIndex, subLast - subIndex).Trim().TrimStart('=');
                    }
                    return Class.Parse<T>(tempValue);
                }
            } else {
                return value;
            }
        }

        List<FileInfo> get_files(Encoding encoding, List<byte[]> file_headers, ref int header_length) {
            List<FileInfo> FI = new List<FileInfo>();
            foreach(KeyValuePair<string, string> kv in this.FILE) {
                FileInfo f = new FileInfo(kv.Key);
                FI.Add(f);

                string file_header = string.Format(
                                    "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n",
                                    kv.Key, f.Name, "application/octet-stream"
                                    );

                byte[] HEADER = encoding.GetBytes(file_header);
                file_headers.Add(HEADER);
                header_length += HEADER.Length + (int)f.Length + 2/*文件结尾换行*/;
            }
            return FI;
        }

        string get_headers(Dictionary<string, string> kvContainer, bool hasFirst = false) {
            string status = hasFirst ? this.read() : null;

            string key_value = string.Empty;
            do {
                key_value = this.read();
                if(string.IsNullOrEmpty(key_value)) {
                    break;
                }
                int key_index = key_value.IndexOf(": ");
                if(key_index <= 0) {
                    Debug.WriteLine("Unkonwn.Header:" + key_value);
                    continue;
                }

                string key = key_value.Substring(0, key_index);
                string value = key_value.Substring(key_index + 2);
                key = string.IsNullOrEmpty(key) ? "" : key.ToLower().Trim();
                value = string.IsNullOrEmpty(value) ? "" : value.Trim();
                if(string.IsNullOrEmpty(key)) {
                    Debug.WriteLine("Unkonwn.Header:" + key_value);
                    continue;
                }

                Debug.WriteLine(key + ": " + value);
                if(kvContainer.ContainsKey(key)) {
                    kvContainer[key] = value;
                } else {
                    kvContainer.Add(key, value);
                }
            } while(!string.IsNullOrEmpty(key_value));

            return status;
        }

        string get_key_value_string(Dictionary<string, string> kvContainer, string seperator, string terminator = "", bool has_last = false, string pre_fix = "") {
            StringBuilder sbTemp = new StringBuilder();
            if(has_last) {
                foreach(KeyValuePair<string, string> kv in kvContainer) {
                    sbTemp.AppendFormat(pre_fix, kv.Key).Append(kv.Key).Append(seperator).Append(kv.Value).Append(terminator);
                }
            } else {
                int kvCount = kvContainer.Count, kvIndex = 0;
                foreach(KeyValuePair<string, string> kv in kvContainer) {
                    sbTemp.AppendFormat(pre_fix, kv.Key).Append(kv.Key).Append(seperator).Append(kv.Value);
                    if(++kvIndex < kvCount) {
                        sbTemp.Append(terminator);
                    }
                }
            }
            return sbTemp.ToString();
        }
    }
}
